Reconstructing Rust Types
A Practical Guide for Reverse Engineers
My Background
- I currently do malware analysis, reverse engineering, and cyber threat intelligence.
- I used to be a C/C++ developer, and Iโm interested in approaching things from the developerโs perspective.
- We were seeing more Rust malware in our analysis queue, and we needed practical skills to deal with them.
Some of my previous work on Rust reversing:
- 2023 - Analysis of Rust code for GDI in the Windows Kernel - Lightning Talk @ RECon
- 2023 - Rust Type Layout Helper - Binary Ninja Plugin
- 2023 - Rust String Slicer - Binary Ninja Plugin
- 2023 - Using panic metadata to recover source code information from Rust binaries - Blog Post
- 2024 - Reversing Rust Binaries: One step beyond strings - Workshop @ NorthSec
- 2024 - Reversing Rust Binaries: One step beyond strings - Workshop @ RECon
The State of Rust Reversing In 2025
Rust RE skills needed for:
- Malware
- Remote access tools (RATs), downloaders, loaders, ransomware, infostealers, adware, etcโฆ
- Windows kernel
- Windows drivers
- Android libraries
- Maybe everything???
Rust is becoming increasingly popular as a general purpose systems programming language.
The State of Rust Reversing In 2025
We have:
- Function signatures for the standard library (e.g.ย shipped with IDA)
- The ability to generate function signatures for third-party libraries (e.g.ย rustbinsign)
- Some basic information on the metadata that you get from dumping a binaryโs strings
- See 2024 - Reversing Rust Binaries: One step beyond strings - Workshop @ RECon
We donโt have:
- Tools for reconstructing Rust types.
- Good systematic explanations of static Rust reversing.
The State of Rust Reversing in 2025
๐ The rustc Book > Codegen Options > strip
Note that, at any level, removing debuginfo only necessarily impacts โfriendlyโ introspection.ย
-Cstripย cannot be relied on as a meaningful security or obfuscation measure, as disassemblers and decompilers can extract considerable information even in the absence of symbols.
(emphasis added)
hahaโฆyesโฆ..we can totally extract considerable informationโฆโฆ.
What weโll cover today
- The basic building blocks of the Rust type system: The programmerโs perspective
- The basic building blocks of the Rust type system: The compilerโs perspective
- Constructing standard library types from the building blocks
- Features of Rust binaries that give information about type layout
Our example: The RustyClaw malware
The RustyClaw malware, first publicly reported by Cisco Talos in October 2024, is a downloader used to deliver a backdoor: ๐ UAT-5647 targets Ukrainian and Polish entities with RomCom malware variants
We will be looking at the sample with SHA-256 hash
b1fe8fbbb0b6de0f1dcd4146d674a71c511488a9eb4538689294bd782df040df
๐ Sample download from MalwareBazaar
This is an x86_32 Windows binary.
Call to core::result::unwrap_failed
Specifically, we will be trying to annotate the code inside this one block as much as possible!
- This block is located inside a function where the malware checks for the Windows version.
Call to core::result::unwrap_failed
Hereโs a preview of the nicely annotated result.
Inside core::result::unwrap_failed
Weโll also be reversing a function called inside this block.
- This actually ends up being the Rust standard library functionย
core::result::unwrap_failed.
Inside core::result::unwrap_failed
The basic building blocks of the Rust type system: The programmerโs perspective
Understanding Rust types, from the source code side.
Learning a little bit of Rust
We canโt learn all of Rust today, but we will need to understand some Rust source code.
- Rust is a very different language from C.
- We need to take the source emitted by our decompiler and go one abstraction level up.
- The Rust standard library is written in Rust, and so is the Rust compiler.
- Sometimes, we will need to look at those when trying to figure out how something works.
- Reducing โmagicโ as much as possible.
- We should be able to find where in the Rust toolchain something comes from, so that we can be prepared if / when something changes.
Itโs important to understand a little bit about the programming ecosystem, and to look at things from the programmerโs perspective, even if you yourself are not a programmer. Something that takes one line of code to write for the programmer may end up being a massive complicated thing for the reverser. If you can read a little bit of Rust code, you can take advantage of the open source nature of the compiler and standard library while trying to figure out how something works.
Reading Rust
We will need to read some Rust source code today, so a crash course for C programmers on some syntax:
- A variable declaration, with a type annotation:
let counter: u64 = 0;- Functions look like this:
fn transform_value(input: u64) -> u64 {
// Function body here
}
let value: u64 = transform_value(10);Reading Rust
- Generics use angle brackets:
let values: Vec<u8> = Vec::new();- Reference types have
&:
fn sum_values(input: &Vec<u8>) -> i64 {
// Function body here
}- To take a reference, also use
&:
let values: Vec<u8> = vec![0, 1, 2];
let sum: i64 = sum_values(&values);Basic types
i64-> Signed 64-bit integeru8-> Unsigned 8-bit integerf32-> 32-bit floating point valueusize,isize-> The size of a pointer, on whatever platform youโre on (thinksize_t,ssize_t)bool-> True, or False. Always a size of 1 byte.()-> The empty โunitโ type.[u64; 128]-> An array of unsigned 64-bit integers, with 128 entries.
Reading Rust
Code examples in this talk are simplified for easier reading and clarity:
- No lifetime annotations
- No
pubkeyword - No
mutkeyword - Some namespaces are expanded for clarity
Things we wonโt talk about
- We will (mostly) not talk about:
- The borrow checker
- Lifetimes
- Mutability
unsafe
- If you want to learn Rust as a programmer, these are important.
- However, they (mostly) donโt affect type layout.
Slices
Syntax: &[T], where T is some type.
- A special type of reference - a sized view into some collection of data.
- An
&[u8]is a sized view into a collection of unsigned 8-bit integers (u8). - This reference contains information about not only the address of the first element, but also about the length of how many elements we want to slice.
- This is essentially a pointer, but with additional length metadata.
Slices
Suppose we have an array of bytes:
let array: [u8; 5] = [0, 1, 2, 3, 4];We can take a slice into that array:
let array_slice = &array[1..3];
println!("Slice contents from index 1 (inclusive) to 3 (exclusive): {:?}", array_slice);Slice contents from index 1 (inclusive) to 3 (exclusive): [1, 2]
And query its length:
println!("Slice length: {}", array_slice.len())Slice length: 2
Strings
We will be focusing on just two string types:
- The primitive type,
&str- Also called a string slice
- A reference / sized view to some static string data somewhere
- The standard library type,
std::string::String- A growable string, (usually) on the heap
The primitive &str type
You will see string slices (&str) for:
- String literals (similar to
const char*):
let PROGRAM_NAME: &str = "tunnel_tool";- Slices of the standard library type,
std::string::String:
let full_name: String = String::from("Cindy Xiao");
let first_name: &str = &full_name[0..5];
println!("Slice of length {}, with data {}", first_name.len(), first_name);Slice of length 5, with data Cindy
An example of &str : Panic path metadata in binaries
See the following for more info:
- 2023 - Rust String Slicer - Binary Ninja Plugin
- 2023 - Using panic metadata to recover source code information from Rust binaries - Blog Post
Structs, and the std::string::String type
- ๐ Docs:
std::string::String
struct String {
vec: Vec<u8>,
}struct Vec<u8> {
buf: RawVec<u8>,
len: usize,
}struct RawVec<u8> {
ptr: *u8, // Some details simplified here
cap: usize,
}The std::string::String type
Putting it all together into a C-like representation:
struct String {
struct Vec<u8> {
struct RawVec<u8> {
uint8_t* ptr;
size_t cap;
} buf;
size_t len;
} vec;
};Enums, i.e.ย tagged unions
enum std::result::Result<i64, String> {
Ok(i64),
Err(String),
}struct Result<i64, String> {
enum {
Ok = 0,
Err = 1,
} discriminant;
union {
int64_t Ok_data;
char* Err_data;
} data;
}Traits (i.e.ย โobject oriented programmingโ in Rust)
- ๐ Docs:
core::fmt::Write- โA trait for writing or formatting into Unicode-accepting buffers or streams.โ
trait core::fmt::Write {
// Required method
fn write_str(&mut self, s: &str) -> Result<(), core::fmt::Error>;
// Provided methods
fn write_char(&mut self, c: char) -> Result<(), core::fmt::Error> { ... }
fn write_fmt(&mut self, args: Arguments) -> Result<(), core::fmt::Error> { ... }
}Traits (i.e.ย โobject oriented programmingโ in Rust)
trait core::fmt::Write {
// Required method
fn write_str(&mut self, s: &str) -> Result<(), core::fmt::Error>;
// Provided methods
fn write_char(&mut self, c: char) -> Result<(), core::fmt::Error> { ... }
fn write_fmt(&mut self, args: Arguments) -> Result<(), core::fmt::Error> { ... }
}impl core::fmt::Write for std::string::String {
fn write_str(&mut self, s: &str) -> Result {
self.push_str(s);
Ok(())
}
fn write_char(&mut self, c: char) -> Result {
self.push(c);
Ok(())
}
}Dynamic dispatch using traits
To do dynamic dispatch without having a concrete type, Rust uses another type of reference: the trait object, e.g.ย &dyn core::fmt::Write.
fn append_woohoo(writeable_type: &mut dyn core::fmt::Write) {
writeable_type.write_str(", woohoo").unwrap();
}
fn main() {
let mut message = String::from("I'm at RE//verse 2025");
println!("{}", message);
append_woohoo(&mut message);
println!("{}", message);
}I'm at RE//verse 2025
I'm at RE//verse 2025, woohoo
Implementing destructors: The Drop Trait
One important trait that will be relevant for us later: The Drop trait.
trait Drop {
// Required method
fn drop(&mut self);
}This is a destructor - it gets called for non-primitive types when values go out of scope!
Implementing the destructor for Vec<T>: impl Drop for Vec<T>
struct Vec<T> {
buf: RawVec<T>,
len: usize,
}
impl<T> Drop for Vec<T> {
fn drop(&mut self) {
unsafe {
ptr::drop_in_place(ptr::slice_from_raw_parts_mut(self.as_mut_ptr(), self.len))
}
}
}The basic building blocks of the Rust type system: The compilerโs perspective
What Rust guarantees: Type Layouts
๐ The Rust Reference > Type Layouts
- The sizes of primitive scalar types (
bool,i64,f32, etc.) are guaranteed. - The sizes of references are guaranteed.
- However, there are different types of references, each with a different (guaranteed) size!
- References to primitive types with known sizes, such as
&i64 - Slices, such as
&str,&[u8], etc. - Trait objects, such as
&dyn core::fmt::Write
- References to primitive types with known sizes, such as
- However, there are different types of references, each with a different (guaranteed) size!
What Rust guarantees: Type Layouts
๐ The Rust Reference > Type Layouts
- โ ๏ธ Structs and enums have no layout guarantees!
- No guaranteed size
- No guaranteed alignment
- No guaranteed field ordering
References: Slices
These include:
- A pointer: To the beginning of the slice
- Metadata attached to the pointer: The length of the slice
References: Trait objects
These include:
- A pointer: To the concrete type
- Metadata attached to the pointer: The trait table
What Rust guarantees: Passing types between functions
โ ๏ธ Rustโs calling convention for Rust-to-Rust function calls is neither stable, nor even defined.
- Often the compiler will pick one of the platformโs usual calling conventions, but this is not guaranteed.
Example from our RustyClaw binary (Windows x86_32) of (sort of) __fastcall:
What Rust guarantees: Passing types between functions
Notice how a single &str is split across two registers here:
struct `&str`
{
uint8_t* _slice_data = "called Result::unwrap() on an Err value"
size_t _slice_len = 0x2b
};References: Pointers and Metadata, through the compiler pipeline
Enums, and their discriminants
A C-like representation of our standard library Result type:
struct std::io::error::Result<Vec<u8>> {
enum {
Ok = 0,
Err = 0x8000000000000000,
} __discriminant;
union {
Vec<u8> Ok_data;
std::io::error::Error* Err_data;
} data;
}Enums, and their discriminants
You will often see this idiom in decompiled Rust binaries:
struct std::io::error::Result<Vec<u8>> read_result;
std::fs::read::inner(&read_result, path_data, path_len);
uint64_t __discriminant = read_result.__discriminant;
if (__discriminant == 0x8000000000000000) {
core::result::unwrap_failed("called `Result::unwrap()` on an `Err` value", 0x2b, &read_result);
/* no return */
} else {
/* Read was successful, do stuff with read_result here... */
}- ๐Docs:
std::mem::discriminant - ๐ The Rust Reference > Discriminants
Example: A Result from RustyClaw
Putting basic building blocks together
Constructing the core::fmt::Arguments standard library type.
Constructing core::fmt::Arguments
Letโs construct the standard libraryโs core::fmt::Arguments type.
- This is useful because it puts together every basic type concept weโve looked at so far - structs, references, slices, enums, etc.
- This is useful, because understanding it is required for understanding string formatting in Rust, which comes up quite often!
String formatting: Printing text with println!
From the programmerโs perspective, doing string formatting is quite easy:
println!("First number is: {}, second number is: {}", 86, 64);First number is: 86, second number is: 64
String formatting: Printing panic strings when aborting / panicking the program
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt").unwrap();
}thread 'main' panicked at src/main.rs:4:49:
called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }
String formatting: Printing panic strings when aborting / panicking the program
- ๐ฝ Source:
library/core/src/result.rs
fn core::result::unwrap_failed(msg: &str, error: &dyn fmt::Debug) -> ! {
panic!("{msg}: {error:?}")
}String formatting: Peeking inside println!
- ๐ Docs:
println!,std::io::stdio::_print - ๐ฝ Source:
std/src/io/stdio.rs
macro_rules! println {
($($arg:tt)*) => {{
$crate::io::_print($crate::format_args_nl!($($arg)*));
}};
}fn std::io::stdio::_print(args: core::fmt::Arguments)The core::fmt::Arguments Type
- ๐ Docs: core::fmt::Arguments
- ๐ฝ Source: core/src/fmt/mod.rs
struct core::fmt::Arguments {
1 pieces: &[&str],
2 fmt: Option<&[core::fmt::rt::Placeholder]>,
3 args: &[core::fmt::rt::Argument],
}- 1
-
An array slice (
&[]), containing the string literals (&str) to put together:["First number is: ", ", second number is: ", "\n"] - 2
- Any formatting specifications (alignment, width, etc.)
- 3
-
An array slice (
&[]), containing the dynamic values (core::fmt::rt::Argument) to display as strings:[86, 64]
Exploding this: &[&str]
- An
&[&str]is:- An array slice (
&[]) - Pointing to a collection of string slices (
&str)
- An array slice (
Exploding this: &[&str]
Exploding this: Option<&[core::fmt::rt::Placeholder]>
- An
Option<&[core::fmt::rt::Placeholder]>is:- A Rust enum, i.e.ย
- tagged union (
Option<>) - Which either contains nothing, or contains a
&[core::fmt::rt::Placeholder]
- A
&[core::fmt::rt::Placeholder]is:- An array slice (
&[]) - Pointing to a collection of
core::fmt::rt::Placeholderstructs
- An array slice (
- 1
-
The
Nonevariant of the union, when it holds no data. - 2
-
The
Somevariant of the union, when it holds an actual array slice (&[]) ofcore::fmt::rt::Placeholdervalues.
Exploding this: Option<&[core::fmt::rt::Placeholder]>
Exploding this: &[core::fmt::rt::Argument]
- A
&[core::fmt::rt::Argument]is:- An array slice (
&[]) - Containing a set of
core::fmt::rt::Argumentstructs
- An array slice (
Exploding this: core::fmt::rt::Argument
struct core::fmt::rt::Argument {
1 value: &Opaque,
2 formatter: fn(_: &Opaque, _: &mut Formatter) -> Result,
}- 1
-
A reference (
&) to a value (Opaque) to format into a string. - 2
-
A pointer to a function (
fn()) which does the actual string formatting.
A C-like representation of core::fmt::rt::Argument
struct Argument<i64>
{
void* value;
Result* (* formatter)(void* value_to_format, Formatter* formatter);
};Exploding this: &[core::fmt::rt::Argument]
The full explosion
struct core::fmt::Arguments
{
struct &[&str] pieces
{
struct &str* data_ptr;
usize length;
},
struct Option<&[core::fmt::rt::Placeholder]> fmt
{
__discriminant_type discriminant;
union
{
&[core::fmt::rt::Placeholder] Some;
struct {} None;
}
}
struct &[core::fmt::rt::Argument] args
{
struct core::fmt::rt::Argument* data_ptr;
usize length;
},
};Back to the RustyClaw core::result::unwrap_failed example
Back to the RustyClaw core::result::unwrap_failed example
Spotting some likely core::fmt::Argument variables
Defining a core::fmt::Argument
struct `core::fmt::Argument`
{
void* value;
void* (* formatter)(void* value, void* formatter_struct);
};After applying the core::fmt::Argument type
Spotting a likely &[core::fmt::Argument] variable
Defining a &[core::fmt::Argument]
struct `&[core::fmt::Argument]` __packed
{
struct `core::fmt::Argument`* _slice_data;
usize _slice_len;
};After applying the &[core::fmt::Argument] type
In the decompiler after applying &[core::fmt::Argument]
Features of Rust binaries that give information about type layout
Allocation functions
- Heap allocations for standard library types, such as for
std::string::String, require a global allocator to be defined. - The Rust standard library provides a default global allocator implementation.
- The details of this will vary by platform.
The standard libraryโs global allocator implementation, on Windows
- ๐ฝ Source: library/std/src/sys/alloc/windows.rs
fn std::sys::pal::windows::alloc::process_heap_alloc(
_heap: MaybeUninit<c::HANDLE>,
flags: u32,
bytes: usize,
) -> *mut c_void
{
let heap = HEAP.load(Ordering::Relaxed);
if core::intrinsics::likely(!heap.is_null()) {
unsafe { HeapAlloc(heap, flags, bytes) }
} else {
1 process_heap_init_and_alloc(MaybeUninit::uninit(), flags, bytes)
}
}- 1
-
Further calls to
HeapAllocinside this function
Deallocation functions
- When types that required heap allocations go out of scope, their destructors are called.
- Types that require heap deallocation implement the
Droptrait - That is, they implement a destructor!
- Types that require heap deallocation implement the
- The Rust standard library provides a default global deallocator implementation.
- The details of this will vary by platform.
The standard libraryโs global deallocator
1fn __rust_dealloc(ptr: *mut u8, size: usize, align: usize);
fn __rdl_dealloc(ptr: *mut u8, size: usize, align: usize) {
unsafe { System.dealloc(ptr, Layout::from_size_align_unchecked(size, align)) }
}- 1
-
This is just a stub; it gets replaced with
__rdl_dealloc, if youโre using the default standard library allocator
The standard libraryโs global deallocator implementation, on Windows
unsafe fn System::dealloc(&self, ptr: *mut u8, layout: Layout) {
let block = {
if layout.align() <= MIN_ALIGN {
ptr
} else {
// The location of the start of the block is stored in the padding before `ptr`.
// SAFETY: Because of the contract of `System`, `ptr` is guaranteed to be non-null
// and have a header readable directly before it.
unsafe { ptr::read((ptr as *mut Header).sub(1)).0 }
}
};
let heap = unsafe { get_process_heap() };
unsafe { HeapFree(heap, 0, block.cast::<c_void>()) };
}Trait objects
Trait object tables (i.e.ย vtables)
Recall the &dyn core::fmt::Write trait object. It includes:
- A pointer: To the concrete type that implements the
core::fmt::Writetrait - Metadata attached to the pointer: A table of pointers to the concrete typeโs functions, which implement that trait (i.e.ย a vtable!)
Trait object tables (i.e.ย vtables)
The vtable attached to trait objects has:
- Size, alignment, and destructor information for the concrete type.
- A fixed layout, such that size, alignment, and destructor information are always in the same places in the table!
Trait object tables (i.e.ย vtables)
struct core::fmt::Write::_vtable alloc::string::String::_vtable =
{
1 void* (* destructor)(void* self) = core::ptr::drop_in_place<alloc::string::String>
2 int64_t size = 0x18
3 int64_t alignment = 0x8
4 void* (* write_str)(void* self, char* str_data, uint64_t str_len) = <alloc::string::String as core::fmt::Write>::write_str
void* (* write_char)(void* self, int32_t character) = <alloc::string::String as core::fmt::Write>::write_char
void* (* write_fmt)(void* self, Arguments* args) = core::fmt::Write::write_fmt
}- 1
- A pointer to the destructor for this concrete type.
- 2
- The size (in bytes) of the concrete type that implements this trait.
- 3
- The alignment (in bytes) of the concrete type that implements this trait.
- 4
- A pointer to this typeโs implementation of a trait method.
Finding trait object tables
- Find a function pointer, followed by 2
usizeconstants, followed by a function pointer - Ensure that the first function pointer is a destructor; that is, make sure it eventually calls
__rust_dealloc/__rdl_dealloc.
Example: Reversing the core::result::unwrap_failed function
- ๐ฝ Source:
library/core/src/result.rs
fn core::result::unwrap_failed(msg: &str, error: &dyn fmt::Debug) -> ! {
panic!("{msg}: {error:?}")
}Printing a debug representation of a struct
You can implement the fmt::Debug trait for your type, to produce a convenient string representation of your type when debugging.
- This can be done via the printing macros (
println!,panic!, etc.), via the{variable_name:?}syntax. - You can also get the compiler to just generate a suitable one for you, by slapping the
#[derive(Debug)]onto your type:
#[derive(Debug)]
struct Coordinates {
x: i64,
y: i64,
}
fn main() {
let cursor_position = Coordinates { x: -100, y: 120 };
println!("{cursor_position:?}");
}Coordinates { x: -100, y: 120 }
The &dyn fmt::Debug trait object
struct &dyn fmt::Debug
{
void* _concrete_type_data;
struct fmt::Debug::_vtable* _vtable;
};Call to core::result::unwrap_failed in RustyClaw
Notice how our metadata-bearing pointer (&dyn fmt::Debug) is split across two variables again!
void core::result::unwrap_failed(
void* error_concrete_type_data, // `&dyn fmt::Debug`._concrete_type_data
struct fmt::Debug::_vtable* error_vtable, // `&dyn fmt::Debug._vtable
struct core::panic::Location* panic_location,
void* msg_data_ptr @ ecx, // `&str`._slice_data
void* msg_len @ edx // `&str`._slice_len
) { [...] }core::result::unwrap_failed(
&unwrapped_err, // error_concrete_type_data (`&dyn fmt::Debug`._concrete_type_data)
&unwrapped_err_vtable, // error_vtable (`&dyn fmt::Debug._vtable)
&panic_location_"src\is_windows7_or_below.rs"_line_37_col_59, // panic_location
"called `Result::unwrap()` on an `Err` value", // msg_data_ptr (`&str`._slice_data)
0x2b // msg_len (`&str`._slice_len)
);Examining the vtable in this call
Note how this vtable likely only has one entry!
Examining the vtable in this call: Defining a vtable type
The fmt::Debug trait only requires the implementation of one method:
trait Debug {
fn fmt(&self, f: &Formatter) -> Result;
}The vtable type will therefore look something like this:
struct fmt::Debug::_vtable __packed
{
void* (* destructor)(void* self);
int32_t size;
int32_t alignment;
int32_t (* fmt)(void* self, struct std::fmt::Formatter* formatter_specification);
};Examining the vtable in this call: Defining the vtable
Peeking inside the fmt implementation
Defining std::fmt::Formatter
struct `std::fmt::Formatter` __packed
{
__padding char _0[0x14];
__padding char _14[4];
__padding char _18[4];
};Defining std::fmt::Formatter
- ๐ Docs:
core::fmt::Formatter
struct Formatter {
flags: u32,
fill: char,
align: Alignment,
width: Option<usize>,
precision: Option<usize>,
buf: &dyn core::fmt::Write,
}Note our trait object, &dyn core::fmt::Write, here!
Looking at &dyn core::fmt::Write
Looking at &dyn core::fmt::Write
struct `&dyn fmt::Write` __packed
{
void* _concrete_type_data;
struct `fmt::Write::_vtable`* _vtable;
};Looking at &dyn core::fmt::Write
struct fmt::Write::_vtable __packed
{
void* (* destructor)(void* self);
uint32_t size;
uint32_t alignment;
int32_t (* write_str)(void* self, char* str_data, usize str_length);
};Defining std::fmt::Formatter: The buf: &dyn core::fmt::Write field
struct `std::fmt::Formatter` __packed
{
__padding char _0[0x14];
struct `&dyn fmt::Write` buf;
};Peeking inside the fmt implementation again
After defining Formatter, &dyn Write, and the Write vtable: We can now see an &str being passed to write_str!
Taking advantage of the default fmt::Debug trait implementation
Recall that you can just use the #[derive(Debug)] to get the compiler to generate a sensible fmt::Debug representation:
#[derive(Debug)]
struct Coordinates {
x: i64,
y: i64,
}
fn main() {
let cursor_position = Coordinates { x: -100, y: 120 };
println!("{cursor_position:?}");
}Coordinates { x: -100, y: 120 }
This prints the name of the type, and all its fields!
A new NulError type
Defining a new NulError type
We actually have the size and alignment of this type already, from the vtable!
Defining a new NulError type
A likely culprit: The std::ffi::NulError type
struct NulError(usize, Vec<u8>); // Struct with anonymous fields๐ Docs: std::ffi:NulError
An error indicating that an interior nul byte was found. While Rust strings may contain nul bytes in the middle, C strings canโt, as that byte would effectively truncate the string. This error is created by theย
newย method onยCString.
A likely culprit: The std::ffi::NulError type
use std::ffi::{CString, NulError};
fn main() {
let err: NulError = CString::new(b"f\0oo".to_vec()).unwrap_err();
println!("{err:?}")
}NulError(1, [102, 0, 111, 111])
struct NulError(
usize, // Position of null byte in data
Vec<u8> // The rest of the data
); // Struct with anonymous fieldsDefining the std::ffi::NulError type
struct `std::ffi::NulError` __packed
{
struct `std::vec::Vec<u8>` _string_data;
uint32_t _position_of_null_byte_in_string_data;
};Defining the std::ffi::NulError type
The _<impl fmt::Debug for std::ffi::NulError>::fmt function
The full vtable for std::ffi::NulError
Our one block of code, nicely annotated
A primer on how to explore Rust internals
There is quite a lot you can figure out just by reading!
- The Rust compiler is open source, and the Rust standard library is open source.
- There is only one production-ready compiler implementation, and only one standard library implementation.
- The Rust compiler is documented: ๐ The rustc book, ๐ Rust Compiler Development Guide
- The Rust standard library is documented: ๐ doc.rust-lang.org/std/, ๐ stdrs.dev
- The Rust language is documented: ๐ The Rust Reference, ๐ The Unsafe Rust Book (Rustonomicon)
Questions
You can also find me at:
- ๐ Mastodon: @cxiao@infosec.exchange
- ๐ฆ Bluesky: @cxiao.net
- ๐ Website: cxiao.net
Acknowledgements
Thank you to:
- My colleagues Ken, Josh, Lilly, and Jรถrg for listening to drafts of this presentation.
- The RE//verse review board for feedback from the dry run, which significantly improved the presentation.
The slide template used here is Grant McDermottโs quarto-revealjs-clean.
Resources
This presentation would not be possible without the huge amount of documentation, blogs, tutorials and public resources published by the Rust community.
- Matt Oswalt: Polymorphism in Rust: https://oswalt.dev/2021/06/polymorphism-in-rust/
- Marco Amann: Rust Dynamic Dispatching deep-dive: https://medium.com/digitalfrontiers/rust-dynamic-dispatching-deep-dive-236a5896e49b
- Raph Levien: Rust container cheat sheet: https://docs.google.com/presentation/d/1q-c7UAyrUlM-eZyTo1pd8SZ0qwA_wYxmPZVOQkoDmH4/edit#slide=id.p
- Mara Bos: Behind the Scenes of Rust String Formatting: format_args!(): https://blog.m-ou.se/format-args/
- Rust to Assembly: Understanding the Inner Workings of Rust: https://www.eventhelix.com/rust/
- fasterthanlime - Peeking inside a Rust Enum: https://fasterthanli.me/articles/peeking-inside-a-rust-enum
- Rust Language Cheat Sheet: https://cheats.rs/
- Primitive Type fn: ABI Compatibility of Rust-to-Rust calls: https://doc.rust-lang.org/core/primitive.fn.html#abi-compatibility
- The Rust Reference: Dynamically Sized Types: https://doc.rust-lang.org/reference/dynamically-sized-types.html
- The Rust Reference: Type Layout: https://doc.rust-lang.org/reference/type-layout.html
- The Rust Reference: Destructors: https://doc.rust-lang.org/reference/destructors.html
- Changes to `u128`/`i128` layout in 1.77 and 1.78: https://blog.rust-lang.org/2024/03/30/i128-layout-update.html
- The Rustonomicon: https://doc.rust-lang.org/nightly/nomicon/
- Exploring dynamic dispatch in Rust: https://alschwalm.com/blog/static/2017/03/07/exploring-dynamic-dispatch-in-rust/
- Rust Deep Dive: Borked Vtables and Barking Cats: https://geo-ant.github.io/blog/2023/rust-dyn-trait-objects-fat-pointers/
- About `vtable_allocation_provider`: https://www.reddit.com/r/rust/comments/11okz75/comment/jbt969m/
- https://github.com/rust-lang/rust/pull/86461/files
- https://github.com/rust-lang/rust/blob/1.83.0/compiler/rustc_middle/src/ty/vtable.rs
- How is `__rust_dealloc` function connected to `__rdl_dealloc` function?: https://users.rust-lang.org/t/how-is-rust-dealloc-function-connectted-to-rdl-dealloc-function/122159
- What is difference between a unit struct and an enum with 0 variants?: https://www.reddit.com/r/rust/comments/1hw19el/what_is_difference_between_a_unit_struct_and_an/